Chapter 7: Enemy Spawning: The Source of Challenge
In this chapter, we will enhance the game's challenge by dynamically spawning enemies. Building on the code from previous chapters, we will focus on designing and implementing enemy spawning logic and behaviors.
1. Enemy Spawning Design Concept
In games, enemies are a primary source of challenge. We aim to implement the following features:
- Randomized Enemy Spawning: Enemies spawn from various directions (top, bottom, left, right).
- Dynamic Speed Adjustment: Enemy speed increases with the player's score.
- Collision Handling: The game ends when enemies collide with the player.
2. Implementing Enemy Spawning Logic
To dynamically spawn enemies, we need to write an Enemy
spawning function. Below is the breakdown of the code logic:
- Spawn Position: Randomly select one of the four edges for enemy spawning.
- Movement Direction: Randomly determine the enemy's movement direction based on its spawn position.
- Speed Adjustment: Dynamically increase the speed based on the current score.
Here is the complete Enemy
function code:
const Enemy = (world: PhysicsWorld.Type, score: number) => {
const dir = math.random(0, 3); // Randomly choose direction
const angle = math.random(dir * 90 + 25, dir * 90 + 180 - 25); // Avoid axis-aligned spawns
let pos = Vec2.zero;
const minW = -hw - 40; const maxW = hw + 40;
const minH = -hh - 40; const maxH = hh + 40;
const randW = math.random(minW, maxW);
const randH = math.random(minH, maxH);
// Determine initial position based on direction
switch (dir) {
case 0: pos = Vec2(minW, randH); break; // Spawn from left
case 1: pos = Vec2(randW, maxH); break; // Spawn from bottom
case 2: pos = Vec2(maxW, randH); break; // Spawn from right
case 3: pos = Vec2(randW, minH); break; // Spawn from top
}
const radian = math.rad(angle); // Convert angle to radians
const velocity = Vec2(math.sin(radian), math.cos(radian))
.normalize()
.mul(200 + score * 2); // Speed scales with score
// Create enemy entity
toNode(
<body world={world} group={0} type={BodyMoveType.Dynamic} linearAcceleration={Vec2.zero}
x={pos.x} y={pos.y} velocityX={velocity.x} velocityY={velocity.y} angle={angle}
onMount={node => {
const enemys = [Animation.enemyFlyingAlt, Animation.enemySwimming, Animation.enemyWalking];
playAnimation(node, enemys[math.random(0, 2)]); // Randomly select an animation
}}>
<disk-fixture radius={40}/> {/* Set collision body */}
</body>
)?.addTo(world);
};
3. Dynamic Enemy Spawning
In the main game logic, we need to spawn enemies periodically. The following code demonstrates how to use world.loop
to spawn enemies based on the player's score:
world.loop(() => {
sleep(0.5); // Spawn an enemy every 0.5 seconds
Enemy(world, score); // Pass the current score
return false; // Repeat indefinitely
});
4. Balancing the Game
To optimize the gameplay experience, you can tweak the following parameters:
- Initial Speed: Modify the
200 + score * 2
base speed value. - Spawn Frequency: Adjust the interval in
sleep(0.5)
. - Score Impact on Speed: Change the weight of the score in the speed formula, e.g.,
200 + score * 4
.
5. Verifying the Gameplay
After implementing enemy spawning, test the following:
- Enemy spawn directions and speeds meet expectations.
- Collision detection works properly.
- Increasing score makes the game progressively challenging.
6. Handling Off-Screen Enemies
Design Concept:
- To prevent the buildup of off-screen enemies, we need to detect when they leave the scene boundaries.
- Enemies leaving the scene will be removed.
We can detect off-screen enemies by adding a static body
with a rectangular sensor in the <physics-world>
. When enemies enter or leave the sensor area, events will trigger accordingly.
Here is the implementation code:
<physics-world>
<body type={BodyMoveType.Static} group={1} onBodyLeave={() => {
// Increment score when an enemy leaves the scene
score++;
}}>
<rect-fixture sensorTag={0} width={width} height={height}/> {/* Sensor matches scene size */}
</body>
</physics-world>
Explanation:
onBodyLeave
is a callback triggered when an enemy leaves the scene.score++
increments the score and updates the score display in real time.
7. Score Display
Design Concept:
- Players earn points by avoiding collisions and letting enemies leave the scene.
- Each enemy that exits the scene increases the player's score.
To display the score, we use a Label
in the game interface. Ensure the label appears only after the "Get Ready!" prompt.
const label = useRef<Label.Type>(); // Create a reference for the score label
<label ref={label} fontName='Xolonium-Regular' fontSize={60} text='0' y={300} visible={false}/>
Explanation:
useRef
allows dynamic updates to the label.- The
visible
property is initially set tofalse
, showing the label only after the game starts.
When the game starts, the label becomes visible after the "Get Ready!" prompt:
world.once(() => {
const msg = toNode(
<label fontName='Xolonium-Regular' fontSize={80} text='Get Ready!' y={200}/>
);
sleep(1); // Remove the prompt after 1 second
msg?.removeFromParent();
if (label.current) {
label.current.visible = true; // Show the score label
}
// Start spawning enemies
// ...
});
8. Complete Enemy Logic Integration
Below is the core integrated logic:
const Game = () => {
inputManager.popContext();
inputManager.pushContext('Game');
let score = 0;
const label = useRef<Label.Type>(); // Create a reference for the score label
Audio.playStream('Audio/House In a Forest Loop.ogg', true);
return (
<clip-node stencil={<Rect/>}>
<physics-world onMount={world => {
Player(world); // Create player
world.once(() => {
// Show "Get Ready!" prompt
const msg = toNode(
<label fontName='Xolonium-Regular' fontSize={80} text='Get Ready!' y={200}/>
);
sleep(1);
msg?.removeFromParent();
if (label.current) {
label.current.visible = true;
}
// Periodically spawn enemies
world.loop(() => {
sleep(0.5);
Enemy(world, score);
return false;
});
});
}}>
<contact groupA={0} groupB={0} enabled={false}/> {/* Disable collisions between enemies */}
<contact groupA={0} groupB={1} enabled/> {/* Enable collisions between player and enemies */}
{/* Sensor to detect off-screen enemies */}
<body type={BodyMoveType.Static} group={1} onBodyLeave={() => {
score++;
if (label.current) {
label.current.text = score.toString();
}
}}>
<rect-fixture sensorTag={0} width={width} height={height}/>
</body>
</physics-world>
<label ref={label} fontName='Xolonium-Regular' fontSize={60} text='0' y={300} visible={false}/>
</clip-node>
);
};
9. Debugging and Verification
After finishing, test the game to ensure:
- Off-screen enemies trigger score increment.
- Score updates correctly.
- Score display logic syncs with game prompts.
10. Chapter Summary
In this chapter, we implemented off-screen enemy detection and the player's scoring mechanism, making the game more complete and engaging. In the next chapter, we will design the game's user interface, including pause functionality, restart buttons, and more.